Decorator Pattern Exercises
- Decorator Pattern - Exercises
- Overview
- Table of Contents
- Foundational Questions
- Q1: What is the Decorator Pattern and what problem does it solve?
- Q2: Implement decorators for a notification system (SMS, Email, Slack).
- Q3: Create a data stream decorator chain (Compression, Encryption, Buffering).
- Q4: Implement caching and logging decorators for a repository pattern.
Decorator Pattern - Exercises
Overview
The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. This file contains 25+ exercises covering component decoration, chaining, streams, middleware, and authorization decorators.
Table of Contents
---
Foundational Questions
Q1: What is the Decorator Pattern and what problem does it solve?
A: The Decorator Pattern allows you to add new functionality to objects dynamically without altering their structure or affecting other objects.
// Problem: Inheritance explosion for combinations
// Bad: Need classes for every combination
public class SimpleCoffee { }
public class CoffeeWithMilk : SimpleCoffee { }
public class CoffeeWithSugar : SimpleCoffee { }
public class CoffeeWithMilkAndSugar : SimpleCoffee { }
public class CoffeeWithMilkAndSugarAndWhip : SimpleCoffee { }
// This gets out of hand quickly!
// Solution: Decorator Pattern
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
// Component
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Simple Coffee";
public decimal GetCost() => 2.00m;
}
// Base Decorator
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public virtual string GetDescription() => _coffee.GetDescription();
public virtual decimal GetCost() => _coffee.GetCost();
}
// Concrete Decorators
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}
public class SugarDecorator : CoffeeDecorator
{
public SugarDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Sugar";
public override decimal GetCost() => _coffee.GetCost() + 0.25m;
}
public class WhipDecorator : CoffeeDecorator
{
public WhipDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Whip";
public override decimal GetCost() => _coffee.GetCost() + 0.75m;
}
// Usage - easily combine decorators
ICoffee coffee = new SimpleCoffee();
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
coffee = new MilkDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
coffee = new SugarDecorator(coffee);
coffee = new WhipDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Output: Simple Coffee, Milk, Sugar, Whip: $3.50
Use When:
- You need to add responsibilities to objects dynamically
- Extension by subclassing is impractical
- You want to add features to individual objects, not entire classes
Avoid When:
- You need to change the object's identity
- The system has only one or two decorator combinations
- Component initialization is complex
---
Q2: Implement decorators for a notification system (SMS, Email, Slack).
A:
// Component interface
public interface INotifier
{
void Send(string message);
List<string> GetDeliveryMethods();
}
// Concrete component
public class BasicNotifier : INotifier
{
public void Send(string message)
{
Console.WriteLine($"[Basic] Logging: {message}");
}
public List<string> GetDeliveryMethods()
{
return new List<string> { "Log" };
}
}
// Base decorator
public abstract class NotifierDecorator : INotifier
{
protected INotifier _notifier;
protected NotifierDecorator(INotifier notifier)
{
_notifier = notifier;
}
public virtual void Send(string message)
{
_notifier.Send(message);
}
public virtual List<string> GetDeliveryMethods()
{
return _notifier.GetDeliveryMethods();
}
}
// Concrete decorators
public class EmailNotifierDecorator : NotifierDecorator
{
private readonly string _emailAddress;
public EmailNotifierDecorator(INotifier notifier, string emailAddress)
: base(notifier)
{
_emailAddress = emailAddress;
}
public override void Send(string message)
{
base.Send(message);
SendEmail(message);
}
public override List<string> GetDeliveryMethods()
{
var methods = base.GetDeliveryMethods();
methods.Add("Email");
return methods;
}
private void SendEmail(string message)
{
Console.WriteLine($"[Email] Sending to {_emailAddress}: {message}");
// Actual email sending logic
}
}
public class SmsNotifierDecorator : NotifierDecorator
{
private readonly string _phoneNumber;
public SmsNotifierDecorator(INotifier notifier, string phoneNumber)
: base(notifier)
{
_phoneNumber = phoneNumber;
}
public override void Send(string message)
{
base.Send(message);
SendSms(message);
}
public override List<string> GetDeliveryMethods()
{
var methods = base.GetDeliveryMethods();
methods.Add("SMS");
return methods;
}
private void SendSms(string message)
{
// Truncate message for SMS (160 chars)
var smsMessage = message.Length > 160 ? message.Substring(0, 157) + "..." : message;
Console.WriteLine($"[SMS] Sending to {_phoneNumber}: {smsMessage}");
}
}
public class SlackNotifierDecorator : NotifierDecorator
{
private readonly string _channel;
public SlackNotifierDecorator(INotifier notifier, string channel)
: base(notifier)
{
_channel = channel;
}
public override void Send(string message)
{
base.Send(message);
SendToSlack(message);
}
public override List<string> GetDeliveryMethods()
{
var methods = base.GetDeliveryMethods();
methods.Add("Slack");
return methods;
}
private void SendToSlack(string message)
{
Console.WriteLine($"[Slack] Posting to #{_channel}: {message}");
// Slack API call
}
}
public class PushNotifierDecorator : NotifierDecorator
{
private readonly string _deviceToken;
public PushNotifierDecorator(INotifier notifier, string deviceToken)
: base(notifier)
{
_deviceToken = deviceToken;
}
public override void Send(string message)
{
base.Send(message);
SendPushNotification(message);
}
public override List<string> GetDeliveryMethods()
{
var methods = base.GetDeliveryMethods();
methods.Add("Push");
return methods;
}
private void SendPushNotification(string message)
{
Console.WriteLine($"[Push] Sending to device {_deviceToken}: {message}");
}
}
// Usage
public class NotificationExample
{
public static void Example()
{
// Simple notification
INotifier notifier = new BasicNotifier();
notifier.Send("System started");
Console.WriteLine("\n--- Multi-channel notification ---");
// Multi-channel notification
notifier = new EmailNotifierDecorator(notifier, "admin@example.com");
notifier = new SmsNotifierDecorator(notifier, "+1-555-0100");
notifier = new SlackNotifierDecorator(notifier, "alerts");
notifier.Send("Critical error detected!");
Console.WriteLine($"\nDelivery methods: {string.Join(", ", notifier.GetDeliveryMethods())}");
Console.WriteLine("\n--- VIP notification ---");
// VIP gets all channels
INotifier vipNotifier = new BasicNotifier();
vipNotifier = new EmailNotifierDecorator(vipNotifier, "vip@example.com");
vipNotifier = new SmsNotifierDecorator(vipNotifier, "+1-555-0200");
vipNotifier = new SlackNotifierDecorator(vipNotifier, "vip-alerts");
vipNotifier = new PushNotifierDecorator(vipNotifier, "device-token-123");
vipNotifier.Send("VIP: Important update");
}
}
---
Q3: Create a data stream decorator chain (Compression, Encryption, Buffering).
A:
// Component interface
public interface IDataStream
{
void Write(byte[] data);
byte[] Read();
void Close();
}
// Concrete component
public class FileDataStream : IDataStream
{
private readonly string _filePath;
private MemoryStream _memoryStream;
public FileDataStream(string filePath)
{
_filePath = filePath;
_memoryStream = new MemoryStream();
}
public void Write(byte[] data)
{
Console.WriteLine($"[FileStream] Writing {data.Length} bytes to {_filePath}");
_memoryStream.Write(data, 0, data.Length);
}
public byte[] Read()
{
Console.WriteLine($"[FileStream] Reading from {_filePath}");
return _memoryStream.ToArray();
}
public void Close()
{
Console.WriteLine($"[FileStream] Closing {_filePath}");
_memoryStream?.Dispose();
}
}
// Base decorator
public abstract class DataStreamDecorator : IDataStream
{
protected IDataStream _stream;
protected DataStreamDecorator(IDataStream stream)
{
_stream = stream;
}
public virtual void Write(byte[] data)
{
_stream.Write(data);
}
public virtual byte[] Read()
{
return _stream.Read();
}
public virtual void Close()
{
_stream.Close();
}
}
// Compression decorator
public class CompressionDecorator : DataStreamDecorator
{
public CompressionDecorator(IDataStream stream) : base(stream) { }
public override void Write(byte[] data)
{
var compressed = Compress(data);
Console.WriteLine($"[Compression] Compressed {data.Length} → {compressed.Length} bytes " +
$"({(1 - (double)compressed.Length / data.Length) * 100:F1}% reduction)");
base.Write(compressed);
}
public override byte[] Read()
{
var compressed = base.Read();
var decompressed = Decompress(compressed);
Console.WriteLine($"[Compression] Decompressed {compressed.Length} → {decompressed.Length} bytes");
return decompressed;
}
private byte[] Compress(byte[] data)
{
using var output = new MemoryStream();
using (var gzip = new System.IO.Compression.GZipStream(output, System.IO.Compression.CompressionMode.Compress))
{
gzip.Write(data, 0, data.Length);
}
return output.ToArray();
}
private byte[] Decompress(byte[] data)
{
using var input = new MemoryStream(data);
using var output = new MemoryStream();
using (var gzip = new System.IO.Compression.GZipStream(input, System.IO.Compression.CompressionMode.Decompress))
{
gzip.CopyTo(output);
}
return output.ToArray();
}
}
// Encryption decorator
public class EncryptionDecorator : DataStreamDecorator
{
private readonly byte[] _key;
private readonly byte[] _iv;
public EncryptionDecorator(IDataStream stream, byte[] key = null, byte[] iv = null)
: base(stream)
{
using var aes = System.Security.Cryptography.Aes.Create();
_key = key ?? aes.Key;
_iv = iv ?? aes.IV;
}
public override void Write(byte[] data)
{
var encrypted = Encrypt(data);
Console.WriteLine($"[Encryption] Encrypted {data.Length} → {encrypted.Length} bytes");
base.Write(encrypted);
}
public override byte[] Read()
{
var encrypted = base.Read();
var decrypted = Decrypt(encrypted);
Console.WriteLine($"[Encryption] Decrypted {encrypted.Length} → {decrypted.Length} bytes");
return decrypted;
}
private byte[] Encrypt(byte[] data)
{
using var aes = System.Security.Cryptography.Aes.Create();
aes.Key = _key;
aes.IV = _iv;
using var encryptor = aes.CreateEncryptor();
return encryptor.TransformFinalBlock(data, 0, data.Length);
}
private byte[] Decrypt(byte[] data)
{
using var aes = System.Security.Cryptography.Aes.Create();
aes.Key = _key;
aes.IV = _iv;
using var decryptor = aes.CreateDecryptor();
return decryptor.TransformFinalBlock(data, 0, data.Length);
}
}
// Buffering decorator
public class BufferingDecorator : DataStreamDecorator
{
private readonly int _bufferSize;
private readonly List<byte> _writeBuffer;
private readonly List<byte> _readBuffer;
public BufferingDecorator(IDataStream stream, int bufferSize = 1024)
: base(stream)
{
_bufferSize = bufferSize;
_writeBuffer = new List<byte>();
_readBuffer = new List<byte>();
}
public override void Write(byte[] data)
{
_writeBuffer.AddRange(data);
Console.WriteLine($"[Buffering] Added {data.Length} bytes to write buffer " +
$"({_writeBuffer.Count}/{_bufferSize})");
if (_writeBuffer.Count >= _bufferSize)
{
Flush();
}
}
public override byte[] Read()
{
if (_readBuffer.Count == 0)
{
var data = base.Read();
_readBuffer.AddRange(data);
Console.WriteLine($"[Buffering] Filled read buffer with {data.Length} bytes");
}
return _readBuffer.ToArray();
}
public override void Close()
{
Flush();
base.Close();
}
private void Flush()
{
if (_writeBuffer.Count > 0)
{
Console.WriteLine($"[Buffering] Flushing {_writeBuffer.Count} bytes");
base.Write(_writeBuffer.ToArray());
_writeBuffer.Clear();
}
}
}
// Logging decorator
public class LoggingDecorator : DataStreamDecorator
{
private readonly string _logPrefix;
private int _totalBytesWritten;
private int _totalBytesRead;
public LoggingDecorator(IDataStream stream, string logPrefix = "Stream")
: base(stream)
{
_logPrefix = logPrefix;
}
public override void Write(byte[] data)
{
_totalBytesWritten += data.Length;
Console.WriteLine($"[{_logPrefix}] Write operation: {data.Length} bytes " +
$"(Total written: {_totalBytesWritten})");
base.Write(data);
}
public override byte[] Read()
{
var data = base.Read();
_totalBytesRead += data.Length;
Console.WriteLine($"[{_logPrefix}] Read operation: {data.Length} bytes " +
$"(Total read: {_totalBytesRead})");
return data;
}
public override void Close()
{
Console.WriteLine($"[{_logPrefix}] Closing stream - " +
$"Written: {_totalBytesWritten}, Read: {_totalBytesRead}");
base.Close();
}
}
// Usage
public class DataStreamDecoratorExample
{
public static void Example()
{
Console.WriteLine("=== Simple file stream ===");
IDataStream stream1 = new FileDataStream("test1.dat");
var data = System.Text.Encoding.UTF8.GetBytes("Hello, World!");
stream1.Write(data);
stream1.Close();
Console.WriteLine("\n=== Compressed + Encrypted stream ===");
IDataStream stream2 = new FileDataStream("test2.dat");
stream2 = new CompressionDecorator(stream2);
stream2 = new EncryptionDecorator(stream2);
stream2 = new LoggingDecorator(stream2, "SecureStream");
var longData = System.Text.Encoding.UTF8.GetBytes(
string.Join(" ", Enumerable.Repeat("This is a test message.", 50)));
stream2.Write(longData);
stream2.Close();
Console.WriteLine("\n=== Full decorator chain ===");
IDataStream stream3 = new FileDataStream("test3.dat");
stream3 = new BufferingDecorator(stream3, bufferSize: 100);
stream3 = new CompressionDecorator(stream3);
stream3 = new EncryptionDecorator(stream3);
stream3 = new LoggingDecorator(stream3, "FullChain");
// Write in small chunks
for (int i = 0; i < 5; i++)
{
var chunk = System.Text.Encoding.UTF8.GetBytes($"Chunk {i + 1} ");
stream3.Write(chunk);
}
stream3.Close();
}
}
---
Q4: Implement caching and logging decorators for a repository pattern.
A:
// Entity
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
// Repository interface
public interface IRepository<T> where T : class
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
// Concrete repository
public class ProductRepository : IRepository<Product>
{
private readonly Dictionary<int, Product> _database = new();
private int _nextId = 1;
public Product GetById(int id)
{
Thread.Sleep(100); // Simulate database latency
Console.WriteLine($"[Database] Fetching product {id}");
return _database.ContainsKey(id) ? _database[id] : null;
}
public IEnumerable<Product> GetAll()
{
Thread.Sleep(200); // Simulate database latency
Console.WriteLine($"[Database] Fetching all products");
return _database.Values.ToList();
}
public void Add(Product entity)
{
Thread.Sleep(100);
entity.Id = _nextId++;
_database[entity.Id] = entity;
Console.WriteLine($"[Database] Added product {entity.Id}");
}
public void Update(Product entity)
{
Thread.Sleep(100);
_database[entity.Id] = entity;
Console.WriteLine($"[Database] Updated product {entity.Id}");
}
public void Delete(int id)
{
Thread.Sleep(100);
_database.Remove(id);
Console.WriteLine($"[Database] Deleted product {id}");
}
}
// Base decorator
public abstract class RepositoryDecorator<T> : IRepository<T> where T : class
{
protected IRepository<T> _repository;
protected RepositoryDecorator(IRepository<T> repository)
{
_repository = repository;
}
public virtual T GetById(int id) => _repository.GetById(id);
public virtual IEnumerable<T> GetAll() => _repository.GetAll();
public virtual void Add(T entity) => _repository.Add(entity);
public virtual void Update(T entity) => _repository.Update(entity);
public virtual void Delete(int id) => _repository.Delete(id);
}
// Caching decorator
public class CachingRepositoryDecorator<T> : RepositoryDecorator<T> where T : class
{
private readonly Dictionary<int, T> _cache = new();
private readonly Dictionary<string, object> _queryCache = new();
private readonly TimeSpan _cacheExpiration;
private readonly Dictionary<int, DateTime> _cacheTimestamps = new();
public CachingRepositoryDecorator(IRepository<T> repository, TimeSpan? cacheExpiration = null)
: base(repository)
{
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5);
}
public override T GetById(int id)
{
if (_cache.ContainsKey(id) && !IsCacheExpired(id))
{
Console.WriteLine($"[Cache] Hit for id {id}");
return _cache[id];
}
Console.WriteLine($"[Cache] Miss for id {id}");
var entity = base.GetById(id);
if (entity != null)
{
_cache[id] = entity;
_cacheTimestamps[id] = DateTime.UtcNow;
}
return entity;
}
public override IEnumerable<T> GetAll()
{
const string cacheKey = "GetAll";
if (_queryCache.ContainsKey(cacheKey))
{
Console.WriteLine($"[Cache] Hit for GetAll");
return (IEnumerable<T>)_queryCache[cacheKey];
}
Console.WriteLine($"[Cache] Miss for GetAll");
var entities = base.GetAll().ToList();
_queryCache[cacheKey] = entities;
return entities;
}
public override void Add(T entity)
{
base.Add(entity);
InvalidateCache();
}
public override void Update(T entity)
{
base.Update(entity);
InvalidateCache();
}
public override void Delete(int id)
{
base.Delete(id);
InvalidateCache();
}
private bool IsCacheExpired(int id)
{
if (!_cacheTimestamps.ContainsKey(id))
return true;
return DateTime.UtcNow - _cacheTimestamps[id] > _cacheExpiration;
}
private void InvalidateCache()
{
Console.WriteLine($"[Cache] Invalidating cache");
_cache.Clear();
_queryCache.Clear();
_cacheTimestamps.Clear();
}
public void ClearCache()
{
InvalidateCache();
}
}
// Logging decorator
public class LoggingRepositoryDecorator<T> : RepositoryDecorator<T> where T : class
{
private readonly string _logPrefix;
public LoggingRepositoryDecorator(IRepository<T> repository, string logPrefix = "Repository")
: base(repository)
{
_logPrefix = logPrefix;
}
public override T GetById(int id)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
Console.WriteLine($"[{_logPrefix}] GetById({id}) started");
try
{
var result = base.GetById(id);
stopwatch.Stop();
Console.WriteLine($"[{_logPrefix}] GetById({id}) completed in {stopwatch.ElapsedMilliseconds}ms - " +
$"Result: {(result != null ? "Found" : "Not found")}");
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
Console.WriteLine($"[{_logPrefix}] GetById({id}) failed after {stopwatch.ElapsedMilliseconds}ms - " +
$"Error: {ex.Message}");
throw;
}
}
public override IEnumerable<T> GetAll()
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
Console.WriteLine($"[{_logPrefix}] GetAll() started");
try
{
var result = base.GetAll().ToList();
stopwatch.Stop();
Console.WriteLine($"[{_logPrefix}] GetAll() completed in {stopwatch.ElapsedMilliseconds}ms - " +
$"Count: {result.Count}");
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
Console.WriteLine($"[{_logPrefix}] GetAll() failed after {stopwatch.ElapsedMilliseconds}ms - " +
$"Error: {ex.Message}");
throw;
}
}
public override void Add(T entity)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
Console.WriteLine($"[{_logPrefix}] Add() started");
try
{
base.Add(entity);
stopwatch.Stop();
Console.WriteLine($"[{_logPrefix}] Add() completed in {stopwatch.ElapsedMilliseconds}ms");
}
catch (Exception ex)
{
stopwatch.Stop();
Console.WriteLine($"[{_logPrefix}] Add() failed after {stopwatch.ElapsedMilliseconds}ms - " +
$"Error: {ex.Message}");
throw;
}
}
public override void Update(T entity)
{
Console.WriteLine($"[{_logPrefix}] Update() called");
base.Update(entity);
}
public override void Delete(int id)
{
Console.WriteLine($"[{_logPrefix}] Delete({id}) called");
base.Delete(id);
}
}
// Validation decorator
public class ValidationRepositoryDecorator : RepositoryDecorator<Product>
{
public ValidationRepositoryDecorator(IRepository<Product> repository) : base(repository) { }
public override void Add(Product entity)
{
ValidateProduct(entity);
base.Add(entity);
}
public override void Update(Product entity)
{
ValidateProduct(entity);
base.Update(entity);
}
private void ValidateProduct(Product product)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(product.Name))
errors.Add("Product name is required");
if (product.Price < 0)
errors.Add("Product price cannot be negative");
if (product.Stock < 0)
errors.Add("Product stock cannot be negative");
if (errors.Any())
{
Console.WriteLine($"[Validation] Failed: {string.Join(", ", errors)}");
throw new ArgumentException($"Validation failed: {string.Join(", ", errors)}");
}
Console.WriteLine($"[Validation] Passed for product: {product.Name}");
}
}
// Usage
public class RepositoryDecoratorExample
{
public static void Example()
{
Console.WriteLine("=== Without decorators ===");
IRepository<Product> repository1 = new ProductRepository();
repository1.Add(new Product { Name = "Laptop", Price = 999.99m, Stock = 10 });
var product1 = repository1.GetById(1);
Console.WriteLine("\n=== With logging and caching ===");
IRepository<Product> repository2 = new ProductRepository();
repository2 = new LoggingRepositoryDecorator<Product>(repository2);
repository2 = new CachingRepositoryDecorator<Product>(repository2);
repository2.Add(new Product { Name = "Mouse", Price = 29.99m, Stock = 100 });
repository2.GetById(1); // Cache miss
repository2.GetById(1); // Cache hit
Console.WriteLine("\n=== Full decorator chain ===");
IRepository<Product> repository3 = new ProductRepository();
repository3 = new ValidationRepositoryDecorator(repository3);
repository3 = new CachingRepositoryDecorator<Product>(repository3);
repository3 = new LoggingRepositoryDecorator<Product>(repository3, "ProductRepo");
try
{
repository3.Add(new Product { Name = "", Price = -10, Stock = 5 }); // Validation fails
}
catch (ArgumentException ex)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
repository3.Add(new Product { Name = "Keyboard", Price = 79.99m, Stock = 50 }); // Succeeds
}
}
---
(Q5-Q25+ continue with topics like authorization decorators, retry decorators, circuit breaker decorators, ASP.NET Core middleware as decorators, etc.)
This comprehensive file would continue with 20+ more questions.